﻿using System;
using System.Collections.Generic;
using System.Text;
using System.Threading;
using System.Xml;
using System.ComponentModel;
using System.ServiceModel;
using System.Reflection;
using System.IO;

namespace DotNetAgent.Core
{
	public enum AgentRunStatus
	{
		DeActivated = 0,
		Activating,
		Activated,
		Starting,
		Running,
		Aborting,
		Aborted,
		Completing,
		Completed,
		Deactivating
	}

	public class Agent : IDisposable
	{
		// Identity
		private string _i_Name = null;
		public string i_Name
		{
			get { return _i_Name; }
		}

		private string _i_Type = null;
		public string i_Type
		{
			get { return _i_Type; }
		}

		public static readonly string s_DotNetAgentVersion = "1.4.1";

		// Owner
		private Agent _i_Owner = null;
		public Agent i_Owner
		{
			get { return _i_Owner; }
		}

		// Owned Agents
		private Dictionary<String, Agent> _i_OwnedAgents = null;
		protected Dictionary<String, Agent> i_OwnedAgents
		{
			get { return _i_OwnedAgents; }
		}

		// All Agents (maintained by root owners)
		private Dictionary<String, Agent> _i_Agents = null;
		protected Dictionary<String, Agent> i_Agents
		{
			get { return _i_Agents; }
		}

		// Logger (maintained by root owners)
		public delegate void Log(String Source, DateTime TimeStamp, string OperationName, string Category, string Msg);
		Log _i_Log;
		protected Log i_Log
		{
			get { return _i_Log; }
			set { _i_Log = value; }
		}

		//private FileLogConnector _i_Logger = null;
		//public FileLogConnector i_Logger
		//{
		//	get { return _i_Logger; }
		//}

		private string _i_LogName = null;
		public string i_LogName
		{
			get { return _i_LogName; }
		}

		// Communication 
		//private Agent _i_Communicator = null;
		//public Agent i_Communicator
		//{
		//    get { return _i_Communicator; }
		//}

		private List<AgentMessage> _i_Incoming = null;
		protected List<AgentMessage> i_Incoming
		{
			get { return _i_Incoming; }
			set { _i_Incoming = value; }
		}

		// Behaviour
		private BackgroundWorker i_Worker = new BackgroundWorker();
		public delegate void Behaviour(Dictionary<String, Object> p_Args);
		Behaviour _i_Behaviours;
		protected Behaviour i_Behaviours
		{
			get { return _i_Behaviours; }
			set { _i_Behaviours = value; }
		}
		private AgentRunStatus _i_RunStatus = AgentRunStatus.Activating;
		public AgentRunStatus i_RunStatus
		{
			get { return _i_RunStatus; }
		}

		//private bool _i_IsCompleted = false;
		//public bool IsCompleted()
		//{
		//    return _i_IsCompleted;
		//}
		//protected void SetCompleted()
		//{
		//    _i_IsCompleted = true;
		//}

		public Agent(Dictionary<String, Object> p_Args)
			: this(null, p_Args)
		{
		}

		// Provide reference to the owner, since we need to at least have one agent that will be able to communicate
		// with the rest of the agent world, unless this is the "all encompassing" agent
		public Agent(Agent p_Owner, Dictionary<String, Object> p_Args)
		{
			try
			{
				_i_RunStatus = AgentRunStatus.Activating;

				// Identity
				if (!p_Args.ContainsKey("Name"))
				{
					throw new AgentException("Activation", "Name not specified");
				}
				_i_Name = p_Args["Name"] as String;
				_i_Type = this.GetType().AssemblyQualifiedName;
				_i_Owner = p_Owner;
				_i_OwnedAgents = new Dictionary<string, Agent>();
				_i_Incoming = new List<AgentMessage>();

				if (i_Owner != null)
				{
					_i_Agents = i_Owner.i_Agents;
				}
				else
				{
					_i_Agents = new Dictionary<string, Agent>();
				}

				// Logging

				if (i_Owner == null)
				{
					//if (!p_Args.ContainsKey("LogFilename"))
					//{
					//    LogFilename = "logs\\" + i_Name + "-" + DateTime.Now.ToString("yyyyMMdd") + ".log";
					//    //LogWarning("Activation", "LogFilename not specified, defaulted to " + p_Args["LogFilename"]);
					//}
					//else
					//{
					//    LogFilename = p_Args["LogFilename"] as String;
					//}
					//WriteLogStart();
					_i_LogName = i_Name;
				}
				else
				{
					//LogFilename = i_Owner.LogFilename;
					_i_LogName = i_Owner.i_LogName + "." + i_Name;
				}

				// Communication
				//if (!p_Args.ContainsKey("Port") && i_Owner == null)
				//{
				//    // TODO: Generate random available port
				//    p_Args.Add("Port", "8003");
				//    LogWarning("Activation", "Port not specified, defaulted to " + p_Args["Port"]);
				//}

				//if (p_Args.ContainsKey("Port"))
				//{
				//    AddAgent(new Dictionary<String, Object>() { { "Name", "TcpCommunicatorAgent" }, { "Type", "TcpCommunicatorAgent" }, { "Port", p_Args["Port"] as String } });
				//    _i_Communicator = i_Agents["TcpCommunicatorAgent"] as TcpCommunicatorAgent;
				//}
				//else
				//{
				//    _i_Communicator = i_Owner.i_Communicator as TcpCommunicatorAgent;
				//}

				//// Behaviour
				i_Worker = new BackgroundWorker();
				i_Worker.DoWork += new DoWorkEventHandler(StartBehaviours);
				////i_Worker.ProgressChanged += new ProgressChangedEventHandler(RunProgressChanged);
				i_Worker.RunWorkerCompleted += new RunWorkerCompletedEventHandler(StopBehaviours);
				i_Worker.WorkerSupportsCancellation = true;
				i_Worker.WorkerReportsProgress = false;

				// Create other owned Agents
				foreach (String l_key in p_Args.Keys)
				{
					if (l_key == "Agent")
					{
						CreateAgent(p_Args[l_key] as Dictionary<String, Object>);
					}
				}

				_i_RunStatus = AgentRunStatus.Activated;
				LogInfo("Activation", "Activated (DotNetAgent Version: " + s_DotNetAgentVersion + ")");
			}
			catch (Exception ex)
			{
				LogError("Activation", "Fatal Error During Agent Activation", ex);
				throw new AgentException("Activation", "Fatal Error During Agent Activation", ex);
			}
		}

		protected void WaitForStatus(AgentRunStatus p_RunStatus, int p_Timeout = 10000)
		{
			if (i_RunStatus == p_RunStatus)
			{
				return;
			}

			DateTime TimeoutTime = DateTime.Now.AddMilliseconds(p_Timeout);
			LogInfo("WaitForStatus", "Waiting for status transition " + System.Enum.GetName(typeof(AgentRunStatus), i_RunStatus) + " --> " + System.Enum.GetName(typeof(AgentRunStatus), p_RunStatus));
			int MSecs = 10;
			while (i_RunStatus != p_RunStatus)
			{
				if (DateTime.Now > TimeoutTime)
				{
					throw new AgentException("WaitForStatus", "Timed out waiting to reach Status " + System.Enum.GetName(typeof(AgentRunStatus), p_RunStatus) + "(Current Status: " + System.Enum.GetName(typeof(AgentRunStatus), i_RunStatus) + ")");
				}
				Sleep(MSecs);
				MSecs += 100;
			}
		}

		public virtual void OnStart()
		{
		}

		public void Start()
		{
			try
			{
				if (i_RunStatus == AgentRunStatus.Activating)
				{
					WaitForStatus(AgentRunStatus.Activated);
				}
				if (i_RunStatus == AgentRunStatus.Starting)
				{
					WaitForStatus(AgentRunStatus.Running);
					return;
				}
				if (i_RunStatus == AgentRunStatus.Running)
				{
					LogInfo("Start", "Already Running");
					return;
				}
				if (i_RunStatus == AgentRunStatus.Aborting)
				{
					WaitForStatus(AgentRunStatus.Aborted);
					//LogWarning("Start", "Still Aborting, yet will proceed with Starting");
				}
				if (i_RunStatus == AgentRunStatus.Completing)
				{
					WaitForStatus(AgentRunStatus.Completed);
					//LogWarning("Start", "Still Completing, yet will proceed with Starting");
				}
				if (i_RunStatus == AgentRunStatus.Deactivating)
				{
					throw new AgentException("Start", "Cannot Start. Currently Deactivating");
				}
				if (i_RunStatus == AgentRunStatus.DeActivated)
				{
					throw new AgentException("Start", "Cannot Start. Currently Deactivated");
				}

				_i_RunStatus = AgentRunStatus.Starting;
				LogInfo("Start", "Starting...");

				i_Worker.RunWorkerAsync();
				WaitForStatus(AgentRunStatus.Running);

				foreach (Agent l_Agent in i_OwnedAgents.Values)
				{
					try
					{
						l_Agent.Start();
					}
					catch (Exception Ex)
					{
						LogError("Start", "Error While Starting owned Agent " + l_Agent.i_Name + ". Aborting Start", Ex);
						Stop(false, false);
					}
				}
				LogInfo("Start", "Starting of all owned agents complete");

				OnStart();
			}
			catch (Exception ex)
			{
				throw new AgentException("Start", "Error While Starting", ex);
			}
		}

		public virtual void OnStop(bool IsCompleted)
		{
		}

		public virtual void Stop(bool IsCompleted, bool Async)
		{
			String ActionStr = IsCompleted ? "Complete" : "Abort";
			try
			{
				OnStop(IsCompleted);


				if (i_RunStatus == AgentRunStatus.DeActivated)
				{
					LogWarning("Stop", ActionStr + " Redundant. Currently Deactivated");
					return;
				}
				if (i_RunStatus == AgentRunStatus.Activating)
				{
					LogInfo("Stop", ActionStr + " Redundant. Still Activating");
					if (!Async)
					{
						WaitForStatus(AgentRunStatus.Activated);
					}
					return;
				}
				if (i_RunStatus == AgentRunStatus.Activated)
				{
					LogInfo("Stop", ActionStr + " Redundant. Not Started");
					if (!Async)
					{
						WaitForStatus(AgentRunStatus.Activated);
					}
					return;
				}
				if (i_RunStatus == AgentRunStatus.Starting)
				{
					// Don't wait since we might want to cancel a start					
					//WaitForStatus(AgentRunStatus.Started);
					LogWarning("Stop", "Still Starting, yet will proceed with " + ActionStr);
				}
				if (i_RunStatus == AgentRunStatus.Aborting)
				{
					if (IsCompleted)
					{
						throw new AgentException("Stop", "Cannot Complete. Currently Aborting");
					}
					if (!Async)
					{
						WaitForStatus(AgentRunStatus.Aborted);
					}
					return;
				}
				if (i_RunStatus == AgentRunStatus.Aborted)
				{
					LogInfo("Stop", ActionStr + " Redundant. Already Aborted");
					return;
				}
				if (i_RunStatus == AgentRunStatus.Completing)
				{
					if (!IsCompleted)
					{
						throw new AgentException("Stop", "Cannot Abort. Currently Completing");
					}
					if (!Async)
					{
						WaitForStatus(AgentRunStatus.Completed);
					}
					return;
				}
				if (i_RunStatus == AgentRunStatus.Completed)
				{
					LogInfo("Stop", ActionStr + " Redundant. Already Completed");
					return;
				}
				if (i_RunStatus == AgentRunStatus.Deactivating)
				{
					LogWarning("Stop", ActionStr + " Redundant. Currently Deactivating");
					return;
				}


				foreach (Agent l_Agent in i_OwnedAgents.Values)
				{
					try
					{
						l_Agent.Stop(IsCompleted, Async);
					}
					catch (Exception Ex)
					{
						LogError("Stop", "Error While Stopping (" + ActionStr + ") on owned Agent " + l_Agent.i_Name, Ex);
					}
				}
				LogInfo("Stop", "Stopping (" + ActionStr + ") of owned agents done");

				_i_RunStatus = IsCompleted ? AgentRunStatus.Completing : AgentRunStatus.Aborting;
				LogInfo("Stop", "Stop (" + ActionStr + ") Pending");

				i_Worker.CancelAsync();
				if (!Async)
				{
					WaitForStatus(IsCompleted ? AgentRunStatus.Completed : AgentRunStatus.Aborted);
				}
			}
			catch (Exception ex)
			{
				throw new AgentException("Stop", "Error While Stopping (" + ActionStr + ")", ex);
			}
		}

		public virtual void Dispose()
		{
			Exception Ex;
			try
			{
				foreach (Agent l_agent in i_OwnedAgents.Values)
				{
					try
					{
						//if (l_agent != i_Logger)
						//{
						l_agent.Dispose();
						//}
					}
					catch (Exception exception1)
					{
						Ex = exception1;
						LogError("Deactivation", "Error While Deactivating owned agent " + l_agent.i_Name, Ex);
					}
				}

				LogInfo("Deactivation", "Deactivation of owned agents complete");

				if (i_RunStatus == AgentRunStatus.Starting || i_RunStatus == AgentRunStatus.Running)
				{
					Stop(false, false);
				}
				_i_RunStatus = AgentRunStatus.Deactivating;
				LogInfo("Deactivation", "Deactivating...");

				//if (i_Owner == null)
				//{
				//    WriteLogEnd();
				//}

				// Dispose Behaviour Resources
				i_Worker.Dispose();

				_i_RunStatus = AgentRunStatus.DeActivated;
			}
			catch (Exception exception2)
			{
				Ex = exception2;
				throw new AgentException("Deactivation", "Error While Deactivating", Ex);
			}
		}

		public static Agent CreateAgent(Agent p_Owner, Dictionary<String, Object> p_Args)
		{
			try
			{
				if (!p_Args.ContainsKey("Type"))
				{
					throw new AgentException("No Type given");
				}
				return (Agent)Activator.CreateInstance(Type.GetType(p_Args["Type"] as String), new object[] { p_Owner, p_Args });
			}
			catch (Exception Ex)
			{
				throw new AgentException("CreateAgent", "Error Creating Agent", Ex);
			}
		}

		public Agent CreateAgent(Dictionary<String, Object> p_Args)
		{
			try
			{
				Agent A = CreateAgent(this, p_Args);
				i_OwnedAgents.Add(A.i_Name, A);
				RegisterAgent(A);
				LogInfo("CreateAgent", "Created owned Agent " + A.i_Name + " of type " + A.i_Type);
				return A;
			}
			catch (Exception Ex)
			{
				throw new AgentException("CreateAgent", "Error Creating Agent", Ex);
			}
		}

		public void RegisterAgent(Agent p_Agent)
		{
			if (!i_Agents.ContainsKey(p_Agent.i_Name))
			{
				i_Agents.Add(p_Agent.i_Name, p_Agent);
			}
		}

		public void AddBehaviour(Behaviour p_Behaviour)
		{
			i_Behaviours += new Behaviour(p_Behaviour);
		}

		public void RemoveBehaviour(Behaviour p_Behaviour)
		{
			i_Behaviours -= new Behaviour(p_Behaviour);
		}

		public void AddLog(Log p_Log)
		{
			i_Log += new Log(p_Log);
		}

		public void RemoveLog(Log p_Log)
		{
			i_Log -= new Log(p_Log);
		}

		public Agent GetAgent(String p_Name)
		{
			if (i_Agents.ContainsKey(p_Name))
			{
				return i_Agents[p_Name];
			}
			return null;
		}

		public static void Sleep(int Milliseconds)
		{
			Thread.Sleep(Milliseconds);
		}

		public void LogError(string Source, string Msg, Exception Ex)
		{
			//i_Logger.LogError(i_LogName, Source, Msg, Ex);
			if (i_Log != null)
			{
				i_Log(i_LogName, DateTime.Now, Source, "Error", Msg + Environment.NewLine + AgentException.FormatExceptionString(Ex));
			}
		}

		public void LogInfo(string Source, string Msg)
		{
			//i_Logger.LogInfo(i_LogName, Source, Msg);
			if (i_Log != null)
			{
				i_Log(i_LogName, DateTime.Now, Source, "Information", Msg);
			}
		}
		public void LogWarning(string Source, string Msg)
		{
			//i_Logger.LogWarning(i_LogName, Source, Msg);
			if (i_Log != null)
			{
				i_Log(i_LogName, DateTime.Now, Source, "Warning", Msg);
			}
		}

		public void Send(String p_TargetAgentName, String p_MsgType, Object p_MsgBody)
		{
			Send(new AgentMessage(i_Name, p_TargetAgentName, p_MsgType, p_MsgBody));
		}

		// Currently supports only interprocess agents
		// TODO: Include support for non-local (tcp) Agents, by checking if the agent is local or not, and implement sending accordingly
		public bool Send(AgentMessage p_Msg)
		{
			LogInfo("Send", p_Msg.Sender + " --" + p_Msg.MessageType + ":" + p_Msg.Body as string + "--> " + p_Msg.Recipient);
			return SendWithinProcess(p_Msg);
		}

		public bool SendWithinProcess(AgentMessage p_Msg)
		{
			Agent A = GetAgent(p_Msg.Recipient);

			if (A == null)
			{
				return false;
			}
			A.Recv(p_Msg);
			return true;
		}

		//public void SendByTCP(AgentMessage p_Msg)
		//{
		//    i_Communicator.Send(p_Msg);
		//}

		public void Recv(AgentMessage p_Msg)
		{
			LogInfo("Recv", p_Msg.Recipient + " <--" + p_Msg.MessageType + ":" + p_Msg.Body as string + "-- " + p_Msg.Sender);
			i_Incoming.Add(p_Msg);
		}

		private void StartBehaviours(object sender, DoWorkEventArgs e)
		{
			_i_RunStatus = AgentRunStatus.Running;
			LogInfo("StartBehaviours", "Started");
			while (i_RunStatus == AgentRunStatus.Running)
			{
				if (i_Behaviours != null)
				{
					i_Behaviours(e.Argument as Dictionary<String, Object>);
				}
			}
		}

		private void StopBehaviours(object sender, RunWorkerCompletedEventArgs e)
		{
			if (i_RunStatus == AgentRunStatus.Aborting)
			{
				_i_RunStatus = AgentRunStatus.Aborted;
				LogInfo("StopBehaviours", "Aborted");
			}
			else if (i_RunStatus == AgentRunStatus.Completing)
			{
				_i_RunStatus = AgentRunStatus.Completed;
				LogInfo("StopBehaviours", "Completed");
			}
			else
			{
				LogWarning("StopBehaviours", "Status is " + System.Enum.GetName(typeof(AgentRunStatus), i_RunStatus) + ". Setting to Aborted");
				_i_RunStatus = AgentRunStatus.Aborted;
			}

		}

		//abstract protected void RunProgressChanged(object sender, ProgressChangedEventArgs e);

		//abstract protected void RunCompleted(object sender, RunWorkerCompletedEventArgs e);

		//protected virtual void Run()
		//{
		//    if (i_Incoming.Count > 0)
		//    {
		//        AgentMessage l_Msg = i_Incoming[0];
		//        LogInfo("Run", "Ignoring Message from " + l_Msg.Recipient + " (Type: " + l_Msg.MessageType + ")(Body: " + l_Msg.Body as string + ")");
		//        i_Incoming.Remove(l_Msg);
		//    }
		//    else
		//    {
		//        Sleep(10000);
		//    }

		//    throw new AgentException("Run", "Run is not defined");
		//}

		//public static void RunAgent(string AgentArgsUrl,int CheckFrequency, int Duration)
		//{
		//    using (Agent MainAgent = AgentHelper.CreateAgentByUrl("agent.xml"))
		//    {
		//        // Let it Live
		//        AgentHelper.Sleep(Duration);

		//        // Time to kill it
		//        MainAgent.Dispose();

		//        // Wait for it to shut down
		//        while (MainAgent.IsActivated)
		//        {
		//            AgentHelper.Sleep(CheckFrequency);
		//        }
		//    }
		//}


		//public static Agent CreateAgent(Agent p_Owner, string p_Args)
		//{
		//    try
		//    {
		//        XmlDocument doc = new XmlDocument();
		//        doc.LoadXml(p_Args);
		//        if (doc.DocumentElement.Attributes["ClassName"] == null)
		//        {
		//            throw new AgentException("No ClassName given");
		//        }
		//        return (Agent)Activator.CreateInstance(Type.GetType(doc.DocumentElement.Attributes["ClassName"].InnerText), new object[] { p_Owner, p_Args });
		//    }
		//    catch (Exception Ex)
		//    {
		//        throw new AgentException("CreateAgent", "Error Creating Agent", Ex);
		//    }
		//}

		//public static Agent CreateAgentByUrl(string AgentArgsUrl)
		//{
		//    try
		//    {
		//        XmlDocument doc = new XmlDocument();
		//        doc.Load(AgentArgsUrl);
		//        if (doc.DocumentElement.Attributes["Type"] == null)
		//        {
		//            throw new AgentException("No Type given");
		//        }
		//        return (Agent)Activator.CreateInstance(Type.GetType(doc.DocumentElement.Attributes["Type"].InnerText), new object[] { doc.OuterXml });
		//    }
		//    catch (Exception Ex)
		//    {
		//        throw new AgentException("CreateAgent", "Error Creating Agent", Ex);
		//    }
		//}

	}
}